koa2 分层搭建框架

中间件

简介

中间件通常带有两个参数 (ctx, next)

ctx 是一个 Koa 的 Context 对象,该对象封装了一个传入的 http 消息,并对该消息进行了相应的响应。 ctx 通常用作上下文对象的参数名称。

通过 Context 对象可以访问到 response 对象和 request 对象。

Context 对象还提供了其 requestresponse 方法的快捷方式。例如可以使用 ctx.type 而不是 ctx.request.type

next 是调用执行下游中间件的函数,在代码执行完成后通过 then 方法返回一个 Promise.

image

image

实现一个日志中间件

1
2
3
4
5
6
7
// logger
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

静态资源加载

koa-static中间件

1
$ npm i koa-static

新建目录 public 作为静态资源的文件夹

root > public > images - javascript - stylesheets

1
2
3
const Koa = require('koa');
const app = new Koa();
app.use(require('koa-static')(__dirname + '/public'));

在 images 下放置图片,可通过 http://localhost:3000/images/demo.png 直接访问图片,不会被当成 get 请求处理

router

koa-router 中间件

1
npm i koa-router

将所有子路由抽离至一个主路由文件

routers > index.js

1
2
3
4
5
6
7
8
9
10
const router = require('koa-router')()

// 引入子路由
const demoRouter = require('./demoRouter')
const apiRouter = require('./apiRouter')

router.use('/demo', demoRouter.routes(), demoRouter.allowedMethods())
router.use('/api', apiRouter.routes(), api.allowedMethods())

module.exports = router

在子路由文件中向外导出规则

routers > demoRouter.js

1
2
3
4
5
6
7
8
9
const router = require("koa-router")();

const DemoController = require("../controller/demoController");

const routers = router
.get("/getNameById", DemoController.getNameById)
.post("/setAgeById", DemoController.setAgeById);

module.exports = routers;

Controller

将请求过程抽离出一层控制层,简单的说 Controller 负责解析用户的输入,处理后返回相应的结果

Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 service 方法处理业务,得到业务结果后封装并返回。

controller > demoController.js

1
2
3
4
5
6
7
8
9
10
11
12
// 导入 service 层
const DemoService = require('../service/demoService');

const DemoController = {
async getNameById(ctx, next) {
let id = ctx.query.id;
let name = await DemoService.getNameById(id);
ctx.body = name;
}
};

module.exports = DemoController;

Service

Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。

比如复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。

service > demoService.js

service 层中读取文件(数据库)后返回数据给 controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');
const path = require('path');

const DemoService = {
async getNameById(id) {
const str = await (() => {
return new Promise((resolve) => {
fs.readFile(path.join(__dirname, '../public/temp.txt'), 'utf-8', (error, data) => {
if (error) throw error;
resolve(data);
})
})
})()
return str;
}
};
module.exports = DemoService;

MySQL

1
$ npm i mysql

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mysql      = require('mysql')
const connection = mysql.createConnection({
host : '127.0.0.1', // 数据库地址
user : 'root', // 数据库用户
password : 'root', // 数据库密码
database : 'record_mysql' // 选中数据库
})

// 执行sql脚本对数据库进行读写
connection.query('SELECT * FROM user_info', (error, results, fields) => {
if (error) throw error
// connected!
console.log(results);
});
// 结束会话
connection.end();

使用连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const mysql      = require('mysql')
// 创建数据池
const pool = mysql.createPool({
host : '127.0.0.1', // 数据库地址
user : 'root', // 数据库用户
password : 'root', // 数据库密码
database : 'record_mysql' // 选中数据库
})

// 在数据池中进行会话操作
pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM user_info', (error, results, fields) => {
console.log(results);
// 结束会话
connection.release();
// 如果有错误就抛出
if (error) throw error;
})
})

async/await封装使用mysql

由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行,现在有了async/await,就可以用同步的写法去操作数据库

抽离相关配置

config.js

1
2
3
4
5
6
7
8
9
10
11
12
const config = {
// 项目启动端口
port: 3000,
database: {
DATABASE: 'record_mysql',
USERNAME: 'root',
PASSWORD: 'root',
HOST: '127.0.0.1'
}
}

module.exports = config

封装 SQL 执行方法

utils > db.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const allConfig = require("../config")
const config = allConfig.database

const mysql = require("mysql");
// 创建数据池
const pool = mysql.createPool({
host: config.HOST, // 数据库地址
user: config.USERNAME, // 数据库用户
password: config.PASSWORD, // 数据库密码
database: config.DATABASE // 选中数据库
});

let query = (sql, values) => {
return new Promise((resolve, reject) => {
pool.getConnection((err, connection) => {
if (err) resolve(err);
connection.query(sql, values, (error, results, fields) => {
if (err) reject(err);
resolve(results);
connection.release();
});
});
});
};

module.exports = query;

service 调用

1
2
3
4
5
6
7
8
const query = require('../utils/db')
const DemoService = {
async getNameById(id) {
let result = await query('SELECT * FROM user_info');
return result;
}
};
module.exports = DemoService;

koa提供了从上下文直接读取、写入cookie的方法

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

koa2 中操作的cookies是使用了npm的cookies模块,源码在https://github.com/pillarjs/cookies,所以在读写cookie的使用参数与该模块的使用一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setCookies(ctx, next) {
ctx.cookies.set(
'cid',
'hello world', {
domain: '127.0.0.1', // 写cookie所在的域名
path: '/demo', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date('2018-05-15'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
}
)
ctx.body = 'cookie is ok'
}

image

session

文件上传

busboy模块

busboy 是用来解析出请求中文件流

封装异步上传图片

utils > upload.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
const inspect = require("util").inspect;
const path = require("path");
const os = require("os");
const fs = require("fs");
const Busboy = require("busboy");

/**
* 同步创建文件目录
* @param {string} dirname 目录绝对地址
* @return {boolean} 创建目录结果
*/
function mkdirsSync(dirname) {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirsSync(path.dirname(dirname))) {
fs.mkdirSync(dirname);
return true;
}
}
}

/**
* 获取上传文件的后缀名
* @param {string} fileName 获取上传文件的后缀名
* @return {string} 文件后缀名
*/
function getSuffixName(fileName) {
let nameList = fileName.split(".");
return nameList[nameList.length - 1];
}

/**
* 上传文件
* @param {object} ctx koa上下文
* @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
* @return {promise}
*/
function uploadFile(ctx, options) {
let req = ctx.req;
let res = ctx.res;
let busboy = new Busboy({ headers: req.headers });

// 获取类型
let fileType = options.fileType || "common";
let filePath = path.join(options.path, fileType);
let mkdirResult = mkdirsSync(filePath);

return new Promise((resolve, reject) => {
console.log("文件上传中...");
let result = {
success: false,
formData: {}
};

// 解析请求文件事件
busboy.on("file", function(fieldname, file, filename, encoding, mimetype) {
let fileName =
Math.random()
.toString(16)
.substr(2) +
"." +
getSuffixName(filename);
let _uploadFilePath = path.join(filePath, fileName);
let saveTo = path.join(_uploadFilePath);

// 文件保存到制定路径
file.pipe(fs.createWriteStream(saveTo));

// 文件写入事件结束
file.on("end", function() {
result.success = true;
result.message = "文件上传成功";

console.log("文件上传成功!");
resolve(result);
});
});

// 解析表单中其他字段信息
busboy.on("field", function(
fieldname,
val,
fieldnameTruncated,
valTruncated,
encoding,
mimetype
) {
console.log("表单字段数据 [" + fieldname + "]: value: " + inspect(val));
result.formData[fieldname] = inspect(val);
});

// 解析结束事件
busboy.on("finish", function() {
console.log("文件上结束");
resolve(result);
});

// 解析错误事件
busboy.on("error", function(err) {
console.log("文件上出错");
reject(result);
});

req.pipe(busboy);
});
}

module.exports = {
uploadFile
};

调用

1
2
3
4
5
6
7
8
9
10
async uploadFile(ctx,next) {
let result = { success: false }
let serverFilePath = path.join( __dirname, '../public/upload-files')
// 上传文件事件
result = await uploadFile( ctx, {
fileType: 'images',
path: serverFilePath
})
ctx.body = result
}

其它中间件

koa-logger

1
2
const koaLogger = require('koa-logger')
app.use(koaLogger())

image

koa-bodyparser

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中

1
$ npm i koa-bodyparser

app.js

1
2
3
4
5
6
7
const bodyparser = require("koa-bodyparser");
// middlewares
app.use(
bodyparser({
enableTypes: ["json", "form", "text"]
})
);

使用

1
ctx.request.body

bodyparser 已经不包含 multipart/form-data 解析了,使用 multer 或 koa-body 一类,官方有中间件列表推荐的

待续

本文结束,感谢您的阅读